package io.gitee.zhangbinhub.admin.conf

import com.fasterxml.jackson.databind.ObjectMapper
import io.gitee.zhangbinhub.acp.boot.exceptions.ServletExceptionHandler
import io.gitee.zhangbinhub.acp.boot.exceptions.WebException
import io.gitee.zhangbinhub.acp.boot.log.LogAdapter
import io.gitee.zhangbinhub.acp.core.common.CommonTools
import io.gitee.zhangbinhub.acp.core.common.log.LogFactory
import io.gitee.zhangbinhub.admin.api.ServerApi
import io.gitee.zhangbinhub.admin.authentication.OauthUserPasswordAuthenticationConverter
import io.gitee.zhangbinhub.admin.authentication.OauthUserPasswordAuthenticationProvider
import io.gitee.zhangbinhub.admin.component.*
import io.gitee.zhangbinhub.admin.constant.AcpConstant
import io.gitee.zhangbinhub.admin.constant.RestPrefix
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
import org.springframework.http.client.support.BasicAuthenticationInterceptor
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.oauth2.core.OAuth2Token
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.client.RestClient
import java.util.*

/**
 * @since JDK 11
 */
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
class AuthorizationServerConfiguration @Autowired constructor(
    serverProperties: ServerProperties,
    private val logAdapter: LogAdapter,
    private val tokenTools: TokenTools,
    private val servletExceptionHandler: ServletExceptionHandler,
    private val objectMapper: ObjectMapper,
    private val authTokenService: AuthTokenService,
    private val authPasswordEncrypt: AuthPasswordEncrypt,
    private val basicUserDetailsService: BasicUserDetailsService,
    private val acpAuthConfiguration: AcpAuthConfiguration,
    private val oAuth2ResourceServerProperties: OAuth2ResourceServerProperties,
    private val authUserService: AuthUserService,
    private val webEndpointProperties: WebEndpointProperties
) {
    private val log = LogFactory.getInstance(this.javaClass)
    private val contextPath: String =
        if (CommonTools.isNullStr(serverProperties.servlet.contextPath)) "" else serverProperties.servlet.contextPath

    @Bean
    @Order(AcpConstant.resourceServerConfiguration - 1)
    @Throws(Exception::class)
    fun acpBasicSecurityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain? {
        // 配置 endpoint 策略
        httpSecurity.csrf { it.ignoringRequestMatchers("/**") }
            .securityMatcher(
                "$contextPath${webEndpointProperties.basePath}",
                "$contextPath${webEndpointProperties.basePath}/**"
            )
            .authorizeHttpRequests { authorizeRequests ->
                authorizeRequests.anyRequest().permitAll()
            }
            .userDetailsService(basicUserDetailsService)
            .httpBasic(Customizer.withDefaults())
        return httpSecurity.build()
    }

    @Bean
    @Order(AcpConstant.resourceServerConfiguration)
    @Throws(Exception::class)
    fun acpAuthorizationServerSecurityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain? {
        val permitAll = ArrayList<String>()
        val security = ArrayList<String>()
        permitAll.add("$contextPath/error")
        permitAll.add("$contextPath/favicon.ico")
        permitAll.add("$contextPath/v3/api-docs/**")
        permitAll.add("$contextPath/*.html")
        permitAll.add("$contextPath/webjars/**")
        permitAll.add("$contextPath${RestPrefix.Open}/**")
        permitAll.add(contextPath + ServerApi.basePath + ServerApi.token)
        acpAuthConfiguration.permitAllPath.forEach { path -> permitAll.add(contextPath + path) }
        acpAuthConfiguration.securityPath.forEach { path -> security.add(contextPath + path) }
        permitAll.forEach { uri -> log.info("permitAll uri: $uri") }
        security.forEach { uri -> log.info("security uri: $uri") }
        log.info("security uri: other any")
        val authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer()
        httpSecurity.with(authorizationServerConfigurer, Customizer.withDefaults())
        // 配置 endpoint 策略
        httpSecurity.csrf { it.ignoringRequestMatchers("/**") }.authorizeHttpRequests { authorizeRequests ->
            authorizeRequests.requestMatchers(authorizationServerConfigurer.endpointsMatcher).permitAll()
                .requestMatchers(AntPathRequestMatcher("/**", RequestMethod.OPTIONS.name)).permitAll()
                .requestMatchers(*security.map { AntPathRequestMatcher(it) }.toTypedArray()).authenticated()
                .requestMatchers(*permitAll.map { AntPathRequestMatcher(it) }.toTypedArray()).permitAll()
                .anyRequest().authenticated()
        }
        // 自定义token端点配置
        val tokenGenerator: OAuth2TokenGenerator<OAuth2Token> =
            DelegatingOAuth2TokenGenerator(OAuth2AccessTokenGenerator(), OAuth2RefreshTokenGenerator())
        authorizationServerConfigurer.tokenEndpoint { tokenEndpoint ->
            tokenEndpoint.accessTokenRequestConverter(
                DelegatingAuthenticationConverter(
                    listOf(
                        OAuth2ClientCredentialsAuthenticationConverter(),
                        OauthUserPasswordAuthenticationConverter()
                    )
                )
            )
            tokenEndpoint.authenticationProvider(
                OAuth2ClientCredentialsAuthenticationProvider(
                    authTokenService, tokenGenerator
                )
            )
            tokenEndpoint.authenticationProvider(
                OauthUserPasswordAuthenticationProvider(
                    logAdapter,
                    tokenTools,
                    authUserService,
                    authPasswordEncrypt,
                    tokenGenerator,
                    authTokenService
                )
            )
            tokenEndpoint.errorResponseHandler { _, response, exception ->
                servletExceptionHandler.responseGlobalException(
                    response,
                    WebException(HttpStatus.UNAUTHORIZED, exception.message)
                )
            }
        }.tokenGenerator(tokenGenerator)
        // 关闭session
        httpSecurity.sessionManagement { it.disable() }
        httpSecurity.oauth2ResourceServer { configurer ->
            configurer.opaqueToken { it.introspector(opaqueTokenIntrospect()) }
            configurer.authenticationEntryPoint { _, response, authException ->
                servletExceptionHandler.responseGlobalException(
                    response,
                    authException
                )
            }
            configurer.accessDeniedHandler { _, response, accessDeniedException ->
                servletExceptionHandler.responseGlobalException(
                    response,
                    accessDeniedException
                )
            }
        }
        return httpSecurity.build()
    }

    /**
     * 设置endpoint的url
     *
     * @return ProviderSettings
     */
    @Bean
    fun authorizationServerSettings(): AuthorizationServerSettings = AuthorizationServerSettings.builder()
        .authorizationEndpoint("/oauth/authorize")
        .tokenEndpoint("/inner/oauth/token")
        .jwkSetEndpoint("/oauth/jwks")
        .tokenRevocationEndpoint("/oauth/revoke")
        .tokenIntrospectionEndpoint("/inner/oauth/introspect")
        .build()

    @Bean
    fun opaqueTokenIntrospect() = AcpOpaqueTokenIntrospect(
        oAuth2ResourceServerProperties.opaquetoken.introspectionUri,
        oauthRestClientBuilder().build(),
        tokenTools
    )

    @Bean("oauthRestClientBuilder")
    fun oauthRestClientBuilder(): RestClient.Builder = RestClient.builder()
        .messageConverters { messageConverters ->
            messageConverters.add(MappingJackson2HttpMessageConverter(objectMapper))
        }.requestInterceptors { requestInterceptors ->
            requestInterceptors.add(
                BasicAuthenticationInterceptor(
                    AcpConstant.resourceServerClientId,
                    AcpConstant.resourceServerClientSecret
                )
            )
        }

    @Bean
    fun bearerTokenResolver() = DefaultBearerTokenResolver().apply {
        this.setAllowFormEncodedBodyParameter(true)
        this.setAllowUriQueryParameter(true)
    }
}
